package snsdbook.engine;

import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Timer;
import java.util.TimerTask;

import snsdbook.engine.message.LoginRequestMessage;
import snsdbook.engine.message.MessageInterface;
import snsdbook.engine.message.ProbeRequest;
import snsdbook.engine.message.SynchronizeRequestMessage;
import util.Util;

public class ClientEngine {
  
  public enum State {kOffline, kOnline, kServerUnreachable};
  public final static long kUpdateFileInitDelay = 1000;
  public final static long kUpdateFileInterval = 5000;
  public final static long kSynchronizeInterval = 10000;
  
  public ClientEngine(String hostname, String port) {
    hostname_ = hostname;
    port_ = Integer.valueOf(port);
    state_ = State.kOffline;
    file_ = null;
    
    synchronize_timer_ = new Timer(/* Daemon = */ true);
    synchronize_timer_.scheduleAtFixedRate(
        new TimerTask() {
          public void run() {
            UpdateSync();
          }
        },
        kUpdateFileInitDelay,
        kUpdateFileInterval);
    Util.Log("Client Engine inintialized");
  }
  
  /**
   * Attempts to Login
   * 
   * @return True if success, false otherwise. Message are sent to Log.
   */
  public synchronized boolean LogIn(String username, String password) {
    assert state_ == State.kOffline;
    Util.Log("Attempting to login as " + username);
    Socket connection = null;
    boolean answer = false;
    try {
      Util.Log("Opening Connection with Server");
      connection = new Socket(hostname_, port_);
      Util.Log("Opening Output Stream");
      ObjectOutputStream obj_os = new ObjectOutputStream(
          connection.getOutputStream());
      Util.Log("Getting Input Stream");
      ObjectInputStream obj_is = new ObjectInputStream(
          connection.getInputStream());      
      Util.Log("Sending Login Request...");
      
      // Send Message
      MessageInterface msg = new LoginRequestMessage(username, password);
      String status = "NONE";
      try {
        status = msg.ProcessAsClient(obj_is, obj_os, file_);
      } catch (AbortError e) {
        status = "TIMEOUT";
      }
      
      obj_is.close();
      obj_os.close();
      if (status.equals("OK")) {
        Util.Log("Login Successful. You are now logged in as " + username);
        answer = true;
        username_ = username;
        state_ = State.kOnline;
      } else if (status.equals("TIMEOUT")) {
        Util.Log("Connection suddenly terminated");
      } else {
        Util.Log("Login Failed");
      }
    } catch (UnknownHostException e) {
      Util.Warning(e);
    } catch (IOException e) {
      Util.Warning(e);
    } finally {
      try {
        if (connection != null) connection.close();
      } catch (Exception e) {
        Util.Warning(e);
      }
    }
    
    return answer;
  }
  
  /**
   * Attempts to Log Out
   */
  public synchronized void LogOut() {
    assert state_ != State.kOffline;
    Util.Log("Attempting to Log Out");
    state_ = State.kOffline;
    Util.Log("Log Out success!");
  }
  
  /**
   * State: kOnline, kOffline, or kServerUnreachable.
   */
  public synchronized State state() { return state_; }
  
  /**
   * Called to change working biebox directory to another place.
   */
  public synchronized void SetDirectory(String directory) {
    Util.Log("Changing directory to " + directory);
    FileManager old = file_;
    file_ = new FileManager(directory);
    if (old != null && !old.directory().equals(file_.directory())) {
      Util.Log("Moving all datas from previous directory(" + old.directory() +
               ") to new one...");
      file_.MoveAll(old);
    }
    
    Util.Log("Changing Directory Successful!");
  }
  
  /**
   * Called periodically to synchronize files with server.
   */
  public synchronized void UpdateSync() {
    if (file_ != null) {
      file_.Update();
      if (state_ == State.kOnline) {
        Sync();
      } else if (state_ == State.kServerUnreachable) {
        TryToReachServer();
      }
    }
  }
  
  /**
   * Attempt to Synchronize data with server
   */
  public synchronized void Sync() {
    Util.Log("Attempting to sync with server...");
    
    // Create the data to be sent.
    File[] present = file_.AllFiles();
    String[] pname = new String[present.length];
    long[] ptime = new long[present.length];
    
    for (int i = 0; i < present.length; ++i) {
      pname[i] = file_.Relativize(present[i].getAbsolutePath());
      ptime[i] = present[i].lastModified();
      assert ptime[i] != 0L;  // otherwise error occurs
    }
    
    String[] del = file_.GetDeletedFiles();
    assert del.length % 2 == 0;
    String[] dname = new String[del.length / 2];
    long[] dtime = new long[del.length / 2];
    
    for (int i = 0; i < del.length / 2; ++i) {
      dname[i] = del[2*i];
      dtime[i] = Long.valueOf(del[2*i+1]);
    }
    
    Util.Log("Attempting to connect to server...");
    Socket socket = null;
    try {
      socket = new Socket(hostname_, port_);
      ObjectOutputStream os = new ObjectOutputStream(socket.getOutputStream());
      ObjectInputStream is = new ObjectInputStream(socket.getInputStream());
      
      Util.Log("Sending synchronization request...");
      MessageInterface request = new SynchronizeRequestMessage(
          username_,
          pname,
          ptime,
          dname,
          dtime);
      request.ProcessAsClient(is, os, file_);
      Util.Log("Sync successful");
      file_.ClearLog();
    } catch (IOException e) {
      Util.Warning(e);
      Util.Log("Switching state to Server Unreachable");
      state_ = State.kServerUnreachable;
    } catch (AbortError e) {
      Util.Log("Connection suddenly terminated");
      Util.Log("Switching state to Server Unreachable");
      state_ = State.kServerUnreachable;
    } finally {
      if (socket != null) {
        try {
          socket.close();
        } catch (Exception e) {
          Util.Warning(e);
        }
      }
    }
  }
  
  /**
   * Attempting to reconnect with server
   */
  public synchronized void TryToReachServer() {
    Util.Log("Attempting to reconnnect with server...");
    Socket socket = null;
    try {
      socket = new Socket(hostname_, port_);
      ObjectOutputStream os = new ObjectOutputStream(socket.getOutputStream());
      ObjectInputStream is = new ObjectInputStream(socket.getInputStream());
      
      String result = new ProbeRequest().ProcessAsClient(is, os, file_);
      if (result.equals("OK")) {
        Util.Log("Connection reestablished.");
        state_ = State.kOnline;
      }
    } catch (IOException e) {
      Util.Warning(e);
    } catch (AbortError e) {
      Util.Log("Connection suddenly terminated");
    } finally {
      if (socket != null) {
        try {
          socket.close();
        } catch (Exception e) {
          Util.Warning(e);
        }
      }
    }
  }
  
  private State state_;
  private String hostname_;
  private int port_;
  private String username_;
  private FileManager file_;
  
  private Timer synchronize_timer_;
  
}
